package client

import (
	"fmt"
	"io/ioutil"
	"mgrok/log"
	"net"
	"net/url"
	"os"
	"path"
	"regexp"
	"strconv"
	"strings"

	"github.com/kardianos/osext"
	"gopkg.in/yaml.v1"
)

// Configuration server config
type Configuration struct {
	HTTPProxy  string `yaml:"http_proxy,omitempty"`
	ServerAddr string `yaml:"server_addr,omitempty"`
	// TrustHostRootCerts bool                            `yaml:"trust_host_root_certs,omitempty"`
	AuthToken string                          `yaml:"auth_token,omitempty"`
	Tunnels   map[string]*TunnelConfiguration `yaml:"tunnels,omitempty"`
	LogTo     string                          `yaml:"-"`
	Path      string                          `yaml:"-"`
	PprofAddr string                          `yaml:"pprof_addr,omitempty"`
}

// TunnelConfiguration tunnel configuration
type TunnelConfiguration struct {
	Subdomain  string `yaml:"subdomain,omitempty"`
	Hostname   string `yaml:"hostname,omitempty"`
	Protocol   string `yaml:"proto,omitempty"`
	Address    string `yaml:"addr,omitempty"`
	HTTPAuth   string `yaml:"auth,omitempty"`
	RemotePort uint16 `yaml:"remote_port,omitempty"`
}

// Protocols  map[string]string `yaml:"proto,omitempty"`

// LoadConfiguration load config
func LoadConfiguration(opts *Options) (config *Configuration, err error) {
	configPath := opts.config
	if configPath == "" {
		configPath = defaultPath()
	}

	configBuf, err := ioutil.ReadFile(configPath)
	if err != nil {
		// failure to read a configuration file is only a fatal error if
		// the user specified one explicitly
		if opts.config != "" {
			err = fmt.Errorf("Failed to read configuration file %s: %v", configPath, err)
			return
		}
	} else {
		log.Info("Reading configuration file %s", configPath)
	}

	// deserialize/parse the config
	config = new(Configuration)
	if err = yaml.Unmarshal(configBuf, &config); err != nil {
		err = fmt.Errorf("Error parsing configuration file %s: %v", configPath, err)
		return
	}

	// try to parse the old .ngrok format for backwards compatibility
	matched := false
	content := strings.TrimSpace(string(configBuf))
	if matched, err = regexp.MatchString("^[0-9a-zA-Z_\\-!]+$", content); err != nil {
		return
	} else if matched {
		config = &Configuration{AuthToken: content}
	}

	// set configuration defaults
	if config.ServerAddr == "" {
		config.ServerAddr = defaultServerAddr
	}

	if config.HTTPProxy == "" {
		config.HTTPProxy = os.Getenv("http_proxy")
	}

	if config.ServerAddr, err = normalizeAddress(config.ServerAddr, "server_addr"); err != nil {
		return
	}

	if config.HTTPProxy != "" {
		var proxyURL *url.URL
		if proxyURL, err = url.Parse(config.HTTPProxy); err != nil {
			return
		}

		if proxyURL.Scheme != "http" && proxyURL.Scheme != "https" {
			err = fmt.Errorf("Proxy url scheme must be 'http' or 'https', got %v", proxyURL.Scheme)
			return
		}
	}

	for name, t := range config.Tunnels {
		if t == nil || len(t.Protocol) == 0 { // t.Protocol == nil ||
			err = fmt.Errorf("Tunnel %s does not specify any protocols to tunnel.\r", name)
			return
		}

		// for k, addr := range t.Protocols {
		// 	tunnelName := fmt.Sprintf("for tunnel %s[%s]", name, k)
		// 	if t.Protocols[k], err = normalizeAddress(addr, tunnelName); err != nil {
		// 		return
		// 	}

		// 	if err = validateProtocol(k, tunnelName); err != nil {
		// 		return
		// 	}
		// }

		tunnelName := fmt.Sprintf("for tunnel %s[%s]", name, t.Protocol)
		if t.Address, err = normalizeAddress(t.Address, tunnelName); err != nil {
			return
		}
		if err = validateProtocol(t.Protocol, tunnelName); err != nil {
			return
		}

		// use the name of the tunnel as the subdomain if none is specified
		if t.Hostname == "" && t.Subdomain == "" {
			// XXX: a crude heuristic, really we should be checking if the last part
			// is a TLD
			if len(strings.Split(name, ".")) > 1 {
				t.Hostname = name
			} else {
				t.Subdomain = name
			}
		}
	}

	// override configuration with command-line options
	config.LogTo = opts.log
	config.Path = configPath
	if opts.authtoken != "" {
		config.AuthToken = opts.authtoken
	}

	switch opts.command {
	// start a single tunnel, the default, simple ngrok behavior
	case "default":
		config.Tunnels = make(map[string]*TunnelConfiguration)
		config.Tunnels["default"] = &TunnelConfiguration{
			Subdomain: opts.subdomain,
			Hostname:  opts.hostname,
			HTTPAuth:  opts.httpauth,
			Protocol:  "",
			Address:   "",
		}

		for _, proto := range strings.Split(opts.protocol, "+") {
			if err = validateProtocol(proto, "default"); err != nil {
				return
			}

			if config.Tunnels["default"].Protocol, err = normalizeAddress(opts.args[0], ""); err != nil {
				return
			}
		}

	// list tunnels
	case "list":
		for name := range config.Tunnels {
			fmt.Println(name)
		}
		os.Exit(0)

	// start tunnels
	case "start":
		if len(opts.args) == 0 {
			err = fmt.Errorf("You must specify at least one tunnel to start")
			return
		}

		requestedTunnels := make(map[string]bool)
		for _, arg := range opts.args {
			requestedTunnels[arg] = true

			if _, ok := config.Tunnels[arg]; !ok {
				err = fmt.Errorf("Requested to start tunnel %s which is not defined in the config file.\r", arg)
				return
			}
		}

		for name := range config.Tunnels {
			if !requestedTunnels[name] {
				delete(config.Tunnels, name)
			}
		}

	case "start-all":
		return

	default:
		err = fmt.Errorf("Unknown command: %s", opts.command)
		return
	}

	return
}

func defaultPath() string {
	filename, _ := osext.Executable()
	dir := path.Dir(filename)

	return path.Join(dir, "mgrok.yaml")
}

func normalizeAddress(addr string, propName string) (string, error) {
	// normalize port to address
	if _, err := strconv.Atoi(addr); err == nil {
		addr = ":" + addr
	}

	host, port, err := net.SplitHostPort(addr)
	if err != nil {
		return "", fmt.Errorf("Invalid address %s '%s': %s", propName, addr, err.Error())
	}

	if host == "" {
		host = "127.0.0.1"
	}

	return fmt.Sprintf("%s:%s", host, port), nil
}

func validateProtocol(proto, propName string) (err error) {
	switch proto {
	case "http", "https", "http+https", "tcp":
	default:
		err = fmt.Errorf("Invalid protocol for %s: %s", propName, proto)
	}

	return
}

// SaveAuthToken save auth token
func SaveAuthToken(configPath, authtoken string) (err error) {
	// empty configuration by default for the case that we can't read it
	c := new(Configuration)

	// read the configuration
	oldConfigBytes, err := ioutil.ReadFile(configPath)
	if err == nil {
		// unmarshal if we successfully read the configuration file
		if err = yaml.Unmarshal(oldConfigBytes, c); err != nil {
			return
		}
	}

	// no need to save, the authtoken is already the correct value
	if c.AuthToken == authtoken {
		return
	}

	// update auth token
	c.AuthToken = authtoken

	// rewrite configuration
	newConfigBytes, err := yaml.Marshal(c)
	if err != nil {
		return
	}

	err = ioutil.WriteFile(configPath, newConfigBytes, 0600)
	return
}
